【ARM Cortex-M开发实战指南(基础篇)】第21章 I2C

开发环境:
MDK:Keil 5.30
STM32CubeMX:V6.4.0
MCU:STM32F4103ZET6

21.1 I2C工作原理

21.1.1 I2C串行总线概述

I2C总线是PHLIPS公司推出的一种双线式半双工串行总线,是具备多主机系统所需的总线裁决和高低速器件同步功能的高性能串行总线。用于连接微控器及外围设备。I2C总线只有两根双向信号线。一根是数据线SDA,另一根是时钟线SCL。

 物理层
1)它只使用两条总线线路 :一条双向串行数据线(SDA),一条串行时钟线(SCL)。见图 1。
2)每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问。
3)多主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。
4)具有三种传输模式 :标准模式的传输速率为 100 Kbit/s ,快速模式为 400 Kbit/s ,高速模式下可达 3.4 Mbit/s,但目前大多12C设备尚不支持高速模式。
5)片上的滤波器可以滤去总线数据线上的毛刺波以保证数据完整。
6)连接到相同总线的 IC 数量受到总线的最大电容 400 pF 限制

ppORKkq.md.png

I2C总线通过上拉电阻接正电源。当总线空闲时,两根线均为高电平。连到总线上的任一器件输出的低电平,都将使总线的信号变低,即各器件的SDA及SCL都是线“与”关系。

ppORN7R.png

每个接到I2C总线上的器件都有唯一的地址。主机与其它器件间的数据传送可以是由主机发送数据到其它器件,这时主机即为发送器。由总线上接收数据的器件则为接收器。

在多主机系统中,可能同时有几个主机企图启动总线传送数据。为了避免混乱, I2C总线要通过总线仲裁,以决定由哪一台主机控制总线。

 协议层
I2C的协议包括起始和停止条件、数据有效性、响应、仲裁、时钟同步和地址广播等环节,由于我们使用的是 STM32 集成的硬件I2C接口,并不需要用软件去模拟 SDA 和SCL 线的时序。

ppORB9K.png

ppORD1O.png

起始信号产生后,所有从机就开始等待主机紧接下来广播的从机地址信号(SLAVE_ADDRESS),在I2C总线上,每个设备的地址都是唯一的。当主机广播的地址与某个设备地址相同时,这个设备就被选中了,没被选中的设备将会忽略之后的数据信号。根据I2C协议,这个从机地址可以是 7 位或 10 位。

在地址位之后,是传输方向的选择位,该位为 0 时,表示后面的数据传输方向是由主机传输至从机。该位为 1 时,则相反。

从机接收到匹配的地址后,主机或从机会返回一个应答(A)或非应答信号,只有接收到应答信号后,主机才能继续发送或接收数据。

若配置的方向传输位为写数据,广播完地址,接收到应答信号后,主机开始正式向从机传输数据(DATA),数据包的大小为 8 位。主机每发送完一个数据,都要等待从机的应答信号(A),重复这个过程,可以向从机传输 N 个数据,这个 N 没有大小限制。当数据传输结束时,主机向从机发送一个停止传输信号(P),表示不再传输数据。

若配置的方向传输位为读数据,广播完地址,接收到应答信号后,从机开始向主机返回数据(DATA),数据包大小也为 8 位。从机每发送完一个数据,都会等待主机的应答信号(A),重复这个过程,可以返回 N 个数据,这个 N 也没有大小限制。当主机希望停止接收数据时,就向从机返回一个非应答信号,则从机自动停止数据传输。

 I2C接口特性
1)STM32 的中等容量和大容量型号的芯片均有多达两个的I2C总线接口。
2)能够工作于多主模式或从模式,分别为主接收器、主发送器、从接收器及从发送器。
3)支持标准模式 100 Kbit/s 和快速模式 400 Kbit/s,不支持高速模式。
4)支持 7 位或 10 位寻址。
5)内置了硬件 CRC 发生器 / 校验器。
6)I2 C 的接收和发送都可以使用 DMA 操作。
7)支持系统管理总线(SMBus)2.0 版。

 I2C 架构
I2C的所有硬件架构就是根据 SCL 线和 SDA 线展开的(其中 SMBALERT 线用于 SMBus)。SCL 线的时序即为I2C 协议中的时钟信号,它由I2C 接口根据时钟控制寄存器(CCR)控制,控制的参数主要为时钟频率。而 SDA 线的信号则通过一系列数据控制架构,在将要发送的数据的基础上,根据协议添加各种起始信号、应答信号、地址信号,实现以 I 2 C 协议的方式发送出去。读取数据时则从 SDA 线上的信号中取出接收到的数据值。发送和接收的数据都被保存在数据寄存器(DR)上。

ppORsje.md.png

21.1.2 I2C总线的数据传送

 数据位的有效性规定

I2C总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。

ppORgHA.png

 起始和终止信号
SCL线为高电平期间,SDA线由高电平向低电平的变化表示起始信号;SCL线为高电平期间,SDA线由低电平向高电平的变化表示终止信号。

ppORRAI.png

起始和终止信号都是由主机发出的,在起始信号产生后,总线就处于被占用的状态;在终止信号产生后,总线就处于空闲状态。连接到I2C总线上的器件,若具有I2C总线的硬件接口,则很容易检测到起始和终止信号。每当发送器件传输完一个字节的数据后,后面必须紧跟一个校验位,这个校验位是接收端通过控制SDA(数据线)来实现的,以提醒发送端数据我这边已经接收完成,数据传送可以继续进行。

 数据传送格式

 字节传送与应答

每一个字节必须保证是8位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)。

ppORf4P.png

由于某种原因从机不对主机寻址信号应答时(如从机正在进行实时性的处理工作而无法接收总线上的数据),它必须将数据线置于高电平,而由主机产生一个终止信号以结束总线的数据传送。

如果从机对主机进行了应答,但在数据传送一段时间后无法继续接收更多的数据时,从机可以通过对无法接收的第一个数据字节的“非应答”通知主机,主机则应发出终止信号以结束数据的继续传送。

当主机接收数据时,它收到最后一个数据字节后,必须向从机发出一个结束传送的信号。这个信号是由对从机的“非应答”来实现的。然后,从机释放SDA线,以允许主机产生终止信号。

 总线的寻址
I2C总线协议有明确的规定:采用7位的寻址字节(寻址字节是起始信号后的第一个字节)。

ppORojg.png

 寻址字节的位定义
D7~D1位组成从机的地址。D0位是数据传送方向位,为“0”时表示主机向从机写数据,为“1”时表示主机由从机读数据。

主机发送地址时,总线上的每个从机都将这7位地址码与自己的地址进行比较,如果相同,则认为自己正被主机寻址,根据R/T位将自己确定为发送器或接收器。

从机的地址由固定部分和可编程部分组成。在一个系统中可能希望接入多个相同的从机,从机地址中可编程部分决定了可接入总线该类器件的最大数目。如一个从机的7位寻址位有4位是固定位,3位是可编程位,这时仅能寻址8个同样的器件,即可以有8个同样的器件接入到该I2C总线系统中。

 数据帧格式
I2C总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。
在起始信号后必须传送一个从机的地址(7位),第8位是数据的传送方向位(R/T),用“0”表示主机发送数据(T),“1”表示主机接收数据(R)。每次数据传送总是由主机产生的终止信号结束。但是,若主机希望继续占用总线进行新的数据传送,则可以不产生终止信号,马上再次发出起始信号对另一从机进行寻址。

在总线的一次数据传送过程中,可以有以下几种组合方式:

A)主机向从机发送数据,数据传送方向在整个过程中不变;

ppOR7uQ.png

注:有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从机向主机传送。A表示应答, A表示非应答(高电平)。S表示起始信号,P表示终止信号。
B)主机在第一个字节后,立即从从机读数据。

ppORHBj.png

C)在传送过程中,当需要改变传送方向时,起始信号和从机地址都被重复产生一次,但两次读/写方向位正好反相。

ppORvCV.md.png

ppz6dbQ.png

要想了解对I2C的主从模式详细了解,参看STM32F10xxx参考手册的I2C接口章节。

21.2 AT24Cxx存储器原理

21.2.1 AT24Cxx概述

AT24C01/02/04/08/16是一个1K/2K/4K/8K/16K位串行CMOS,EEPROM内部含有128/256/512/1024/2048个8位字节CATALYST公司的先进CMOS技术实质上减少了器件的功耗,AT24C01/02有一个8字节页写缓冲器AT24C04/08/16有一个16字节页写缓冲器,该器件通过I2C总线接口进行操作有一个专门的写保护功能。AT24C01/02每页有8个字节,分别为16/32页;AT24C04/08/16每页有16个字节,分别为32/64/128页。

工作特点

 与400KHz I2C总线兼容
 1.8到6.0伏工作电压范围
 低功耗CMOS技术
 写保护功能当WP为高电平时进入写保护状态
 页写缓冲器
 自定时擦写周期
 100万次编程/擦除周期
 可保存数据100年
 8脚DIP SOIC或TSSOP封装
 温度范围商业级和工业级

AT24Cxx的引脚定义如下:

ppz6sCq.md.png

Note: For use of 5-lead SOT23, the software A2, A1, and A0 bits in the device address word must be set to zero toproperly communicate.

ppORQhV.md.png

21.2.2总线时序

I2C总线时序如下:

ppz662V.md.jpg

其读写周期的的电压范围如下:

ppOR31U.md.jpg

写周期时间是指从一个写时序的有效停止信号到内部编程/擦除周期结束的这一段时间。在写周期期间,总线接口电路禁能,SDA保持为高电平,器件不响应外部操作。

21.2.3器件寻址

主器件通过发送一个起始信号启动发送过程,然后发送它所要寻址的从器件的地址。8位从器件地址的高4位固定为(1010)。接下来的3位(A2、A1、A0)为器件的地址位,用来定义哪个器件以及器件的哪个部分被主器件访问,上述8个AT24C01/02,4个AT24C04,2个AT24C08,1个AT24C16可单独被系统寻址。从器件8位地址的最低位,作为读写控制位。“1”表示对从器件进行读操作,“0”表示对从器件进行写操作。在主器件发送起始信号和从器件地址字节后,AT24C01/02/04/08/16监视总线并当其地址与发送的从地址相符时响应一个应答信号(通过SDA线)。AT24C01/02/04/08/16再根据读写控制位(R/W)的状态进行读或写操作。

ppz6RrF.jpg

 字节写
在字节写模式下,主器件发送起始命令和从器件地址信息(R/W)位置发给从器件,在从器件产生应答信号后,主器件发送AT24Cxx的字节地址,主器件在收到从器件的另一个应答信号后,再发送数据到被寻址的存储单元。AT24Cxx再次应答,并在主器件产生停止信号后开始内部数据的擦写,在内部擦写过程中,AT24Cxx不再应答主器件的任何请求。

ppz655R.md.jpg

 页写

用页写,AT24C01/02可一次写入8 个字节数据,AT24C04/08/16可以一次写入16个字节的数据。页写操作的启动和字节写一样,不同在于传送了一字节数据后并不产生停止信号。主器件被允许发送P(AT24C01:P=7;AT24C02/04/08/16:P=15)个额外的字节。每发送一个字节数据后AT24Cxx产生一个应答位并将字节地址低位加1,高位保持不变。

如果在发送停止信号之前主器件发送超过P+1个字节,地址计数器将自动翻转,先前写入的数据被覆盖。
接收到P+1字节数据和主器件发送的停止信号后,AT24Cxx启动内部写周期将数据写到数据区。所有接收的数据在一个写周期内写入AT24Cxx。

ppz6Oqe.md.jpg

 读字节
读操作允许主器件对寄存器的任意字节进行读操作,主器件首先通过发送起始信号、从器件地址和它想读取的字节数据的地址执行一个写操作。在AT24Cxx应答之后,主器件重新发送起始信号和从器件地址,此时R/W位置1,AT24Cxx响应并发送应答信号,然后输出所要求的一个8位字节数据,主器件不发送应答信号但产生一个停止信号。

ppzcZIs.md.jpg

 顺序读
在AT24Cxx发送完一个8位字节数据后,主器件产生一个应答信号来响应,告知AT24Cxx主器件要求更多的数据,对应每个主机产生的应答信号AT24Cxx将发送一个8位数据字节。当主器件不发送应答信号而发送停止位时结束此操作。

从AT24Cxx输出的数据按顺序由N到N+1输出。读操作时地址计数器在AT24Cxx整个地址内增加,这样整个寄存器区域可在一个读操作内全部读出,当读取的字节超过E(对于24WC01,E=127;对24C02,E=255;对24C04,E=511;对24C08,E=1023;对24C16,E=2047)计数器将翻转到零并继续输出数据字节。

ppzclsU.md.jpg

 典型应用
ATC02的典型电路如下:

ppzc1LF.jpg

根据AT24C02的芯片资料,我们会发现AT24C02有三个地址A0,A1,A2。同时,我们会在资料的Device Address介绍发现I2C器件一共有七位地址码,还有一位是读/写(R/W)操作位,而在AT24C02的前四位已经固定为1010。R/W为1则为 读操作,为0则为写操作。R/W位我们要设置为0(写操作)。

规则为:1010(A0)(A1)(A2)(R/W)
例子1:
那么对应的A0,A1,A2都是接的VCC,所以为A0=1,A1=1,A2=1;可以知道AT24C02的从设备写地址为10101110(0xae),读设备地址为10101111(0xaf)。

例子2:
那么对应的A0,A1,A2都是接的GND,所以为A0=0,A1=0,A2=0;可以知道AT24C02的从设备写地址为10100000(0xa0),读设备地址为10100001(0xa1)。

21.3 I2C寄存器描述

I2C有6类寄存器,详细的介绍请参考STM32F10XXX参考手册的I2C寄存器描述部分。在这里笔者只讲最重要的2个寄存器。

 数据寄存器
数据寄存器的详细描述如下所示。

ppzctiR.md.png

 时钟寄存器
时钟寄存器是I2C中比较重要的一个寄存器,时钟信号的信号的稳定是I2C正常工作的前提。

ppzcURx.md.png

ppzcBLD.md.png

21.4硬件设计及连接

本文是使用I2C协议对EEPROM进行读写操作,具体的硬件连接如下。

ppzcyod.png

从硬件链接可以得到AT24C02的地址是0xA0。

21.5硬件I2C

21.5.1具体代码实现-标准库

首先看看I2C的初始化。这有两部分。

一部分是I2C的GPIO初始化。

/**
  * @brief  I2C1 I/O配置
  * @param  无
  * @retval 无
  */
static void I2C_GPIO_Config(void)
{
    GPIO_InitTypeDef  GPIO_InitStructure; 

    /* 使能与 I2C1 有关的时钟 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);  

    /* PB6-I2C1_SCL、PB7-I2C1_SDA*/
    GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;        // 开漏输出
    GPIO_Init(GPIOB, &GPIO_InitStructure);
}

另外一部分就是配置I2C的参数。

/**
  * @brief  I2C 工作模式配置
  * @param  无
  * @retval 无
  */
static void I2C_Mode_Config(void)
{
    I2C_InitTypeDef  I2C_InitStructure; 

    /* I2C 配置 */
    I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;

    /* 高电平数据稳定,低电平数据变化 SCL 时钟线的占空比 */
    I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;

    I2C_InitStructure.I2C_OwnAddress1 =I2C1_OWN_ADDRESS7; 
    I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ;

    /* I2C的寻址模式 */
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;

    /* 通信速率 */
    I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;

    /* I2C1 初始化 */
    I2C_Init(I2C1, &I2C_InitStructure);

    /* 使能 I2C1 */
    I2C_Cmd(I2C1, ENABLE);
}

主要配置I2C模式、低电平占空比、I2C寻址模式以及通信速率,最后使能I2C设备。
初始化完成后就是对AT24C02的读写操作,严格按照相应的时序操作就行。

 字节写

在字节写模式下,向AT24C02中写数据时序如下:

ppzc4OS.md.png

操作时序如下:

1.MCU先发送一个开始信号(START)启动总线
2.接着跟上首字节,发送器件写操作地址(DEVICE ADDRESS)+写数据(0xA0)
3.等待应答信号(ACK)
4.发送数据的存储地址。24C02一共有256个字节的存储空间,地址从0x00~0xFF,想把数据存储在哪个位置,此刻写的就是哪个地址。
5.发送要存储的数据,在写数据的过程中,AT24C02会回应一个“应答位0”,则表明写AT24C02数据成功,如果没有回应答位,说明写入不成功。
6.发送结束信号(STOP)停止总线。

代码很简单,跟着时序来就行。

/**
  * @brief   写一个字节到I2C EEPROM中
  * @param   
  * @arg pBuffer:缓冲区指针
  * @arg WriteAddr:写地址 
  * @retval  无
  */
void I2C_EE_ByteWrite(uint8_t* pBuffer, uint8_t WriteAddr)
{
    /* Send STRAT condition */
    I2C_GenerateSTART(I2C1, ENABLE);

    /* Test on EV5 and clear it */
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));  

    /* Send EEPROM address for write */
    I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);

    /* Test on EV6 and clear it */
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

    /* Send the EEPROM's internal address to write to */
    I2C_SendData(I2C1, WriteAddr);

    /* Test on EV8 and clear it */
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    /* Send the byte to be written */
    I2C_SendData(I2C1, *pBuffer); 

    /* Test on EV8 and clear it */
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    /* Send STOP condition */
    I2C_GenerateSTOP(I2C1, ENABLE);
}

 页写
用页写,AT24C01可一次写入8 个字节数据,AT24C02/04/08/16可以一次写入16个字节的数据。页写操作的启动和字节写一样,不同在于传送了一字节数据后并不产生停止信号。每发送一个字节数据后AT24Cxx产生一个应答位并将字节地址低位加1,高位保持不变。

如果在发送停止信号之前主器件发送超过P+1个字节,地址计数器将自动翻转,先前写入的数据被覆盖。
接收到P+1字节数据和主器件发送的停止信号后,AT24Cxx启动内部写周期将数据写到数据区。所有接收的数据在一个写周期内写入AT24Cxx。

ppz6Oqe.md.jpg

代码很简单,和字节写不同的是,数据会一直发,直到主机发送停止信号。

/**
  * @brief   在EEPROM的一个写循环中可以写多个字节,但一次写入的字节数
  *          不能超过EEPROM页的大小,AT24C02每页有8个字节
  * @param   
  * @arg pBuffer:缓冲区指针
  * @arg WriteAddr:写地址
  * @arg NumByteToWrite:写的字节数
  * @retval  无
  */
void I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint8_t NumByteToWrite)
{
    while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); // Added by Najoua 27/08/2008

    /* Send START condition */
    I2C_GenerateSTART(I2C1, ENABLE);

    /* Test on EV5 and clear it */
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); 

    /* Send EEPROM address for write */
    I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);

    /* Test on EV6 and clear it */
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));  

    /* Send the EEPROM's internal address to write to */    
    I2C_SendData(I2C1, WriteAddr);  

    /* Test on EV8 and clear it */
    while(! I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    /* While there is data to be written */
    while(NumByteToWrite--)  
    {
        /* Send the current byte */
        I2C_SendData(I2C1, *pBuffer); 

        /* Point to the next byte to be written */
        pBuffer++; 

        /* Test on EV8 and clear it */
        while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
    }

    /* Send STOP condition */
    I2C_GenerateSTOP(I2C1, ENABLE);
}

 任意写
在实际过程中,我们经常需要任意写数据,这里就调用页写的操作,来实现任意字节的写操作。

/**
  * @brief   将缓冲区中的数据写到I2C EEPROM中
  * @param   
  * @arg pBuffer:缓冲区指针
  * @arg WriteAddr:写地址
  * @arg NumByteToWrite:写的字节数
  * @retval  无
  */
void I2C_EE_BufferWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint16_t NumByteToWrite)
{
    uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;

    Addr = WriteAddr % I2C_PageSize;
    count = I2C_PageSize - Addr;
    NumOfPage =  NumByteToWrite / I2C_PageSize;
    NumOfSingle = NumByteToWrite % I2C_PageSize;

    /* If WriteAddr is I2C_PageSize aligned  */
    if(Addr == 0) 
    {
        /* If NumByteToWrite < I2C_PageSize */
        if(NumOfPage == 0) 
        {
            I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
            I2C_EE_WaitEepromStandbyState();
        }
        /* If NumByteToWrite > I2C_PageSize */
        else 
        {
            while(NumOfPage--)
            {
                I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize); 
                I2C_EE_WaitEepromStandbyState();
                WriteAddr +=  I2C_PageSize;
                pBuffer += I2C_PageSize;
            }
            if(NumOfSingle!=0)
            {
                I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
                I2C_EE_WaitEepromStandbyState();
            }
        }
    }
    /* If WriteAddr is not I2C_PageSize aligned  */
    else 
    {
        /* If NumByteToWrite < I2C_PageSize */
        if(NumOfPage== 0) 
        {
            I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
            I2C_EE_WaitEepromStandbyState();
        }
        /* If NumByteToWrite > I2C_PageSize */
        else
        {
            NumByteToWrite -= count;
            NumOfPage =  NumByteToWrite / I2C_PageSize;
            NumOfSingle = NumByteToWrite % I2C_PageSize;

            if(count != 0)
            {
                I2C_EE_PageWrite(pBuffer, WriteAddr, count);
                I2C_EE_WaitEepromStandbyState();
                WriteAddr += count;
                pBuffer += count;
            } 

            while(NumOfPage--)
            {
                I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);
                I2C_EE_WaitEepromStandbyState();
                WriteAddr +=  I2C_PageSize;
                pBuffer += I2C_PageSize;  
            }
            if(NumOfSingle != 0)
            {
                I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle); 
                I2C_EE_WaitEepromStandbyState();
            }
        }
    }
}

主要分为两种情况,写的地址正好是一页的开始,另外一种是在一页的中间。不管如何,始终遵循的原则就是最大智能写一页,可以从一页的中间开始。

 读字节
读操作允许主器件对寄存器的任意字节进行读操作,主器件首先通过发送起始信号、从器件地址和它想读取的字节数据的地址执行一个写操作。在AT24Cxx应答之后,主器件重新发送起始信号和从器件地址,此时R/W位置1,AT24Cxx响应并发送应答信号,然后输出所要求的一个8位字节数据,主器件不发送应答信号但产生一个停止信号。

ppzcZIs.md.jpg

读取字节的时序如下:

1.MCU先发送一个开始信号(START)启动总线
2.接着跟上首字节,发送器件写操作地址(DEVICE ADDRESS)+写数据(0xA0)
注意:这里写操作是为了要把所要读的数据的存储地址先写进去,告诉AT24Cxx要读取哪个地址的数据。
3.发送要读取内存的地址(WORD ADDRESS),通知AT24Cxx读取要哪个地址的信息。
4.重新发送开始信号(START)。
5.发送设备读操作地址(DEVICE ADDRESS)对AT24Cxx进行读操作 (0xA1)。
6.AT24Cxx会自动向主机发送数据,主机读取从器件发回的数据,在读一个字节后,MCU会回应一个应答信号(ACK)。
7.发送一个“非应答位NAK(1)”。发送结束信号(STOP)停止总线。

 顺序读
在AT24Cxx发送完一个8位字节数据后,主器件产生一个应答信号来响应,告知AT24Cxx主器件要求更多的数据,对应每个主机产生的应答信号AT24Cxx将发送一个8位数据字节。当主器件不发送应答信号而发送停止位时结束此操作。

从AT24Cxx输出的数据按顺序由N到N+1输出。读操作时地址计数器在AT24Cxx整个地址内增加,这样整个寄存器区域可在一个读操作内全部读出,当读取的字节超过E(对于24WC01,E=127;对24C02,E=255;对24C04,E=511;对24C08,E=1023;对24C16,E=2047)计数器将翻转到零并继续输出数据字节。

ppzclsU.md.jpg

我们常用的方式就是连续读取,代码很简单。

/**
  * @brief   从EEPROM里面读取一块数据 
  * @param   
  * @arg pBuffer:存放从EEPROM读取的数据的缓冲区指针
  * @arg WriteAddr:接收数据的EEPROM的地址
  * @arg NumByteToWrite:要从EEPROM读取的字节数
  * @retval  无
  */
void I2C_EE_BufferRead(uint8_t* pBuffer, uint8_t ReadAddr, uint16_t NumByteToRead)
{  
    //*((u8 *)0x4001080c) |=0x80; 
    while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); // Added by Najoua 27/08/2008    

    /* Send START condition */
    I2C_GenerateSTART(I2C1, ENABLE);
    //*((u8 *)0x4001080c) &=~0x80;

    /* Test on EV5 and clear it */
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    /* Send EEPROM address for write */
    I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);

    /* Test on EV6 and clear it */
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

    /* Clear EV6 by setting again the PE bit */
    I2C_Cmd(I2C1, ENABLE);

    /* Send the EEPROM's internal address to write to */
    I2C_SendData(I2C1, ReadAddr);  

    /* Test on EV8 and clear it */
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    /* Send STRAT condition a second time */  
    I2C_GenerateSTART(I2C1, ENABLE);

    /* Test on EV5 and clear it */
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    /* Send EEPROM address for read */
    I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Receiver);

    /* Test on EV6 and clear it */
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));

    /* While there is data to be read */
    while(NumByteToRead)
    {
        if(NumByteToRead == 1)
        {
            /* Disable Acknowledgement */
            I2C_AcknowledgeConfig(I2C1, DISABLE);

            /* Send STOP Condition */
            I2C_GenerateSTOP(I2C1, ENABLE);
        }
        /* Test on EV7 and clear it */
        if(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED))  
        {      
            /* Read a byte from the EEPROM */
            *pBuffer = I2C_ReceiveData(I2C1);

            /* Point to the next location where the byte read will be saved */
            pBuffer++;

            /* Decrement the read bytes counter */
            NumByteToRead--;
         }   
    }
    /* Enable Acknowledgement to be ready for another reception */
    I2C_AcknowledgeConfig(I2C1, ENABLE);
}

最后看下主函数吧。

/**
  * @brief  主函数
  * @param  无
  * @retval 无
  */
int main(void)
{ 
    /*SysTick Init*/
    SysTick_Init();

    /* USART1 config 115200 8-N-1 */
    USART_Config();

    printf("\r\n I2C外设(AT24C02)读写测试例程 \r\n");
    LED_GPIO_Config();

    /* I2C 外设初(AT24C02)始化 */
    I2C_EE_Init();

    I2C_Test();

    while (1)
    {
    }
}

很简单,往AT24C02中写入数据,然后再读取数据,读写测试的函数如下:

/**
  * @brief  I2C(AT24C02)读写测试
  * @param  无
  * @retval 无
  */
void I2C_Test(void)
{
    uint16_t i;

    printf("写入的数据\r\n");

    for ( i=0; i<=255; i++ ) //填充缓冲
    {   
        I2c_Buf_Write[i] = i;

        printf("0x%02X ", I2c_Buf_Write[i]);
        if(i%16 == 15)
        {
            printf("\n\r");
        }
    }
    //将I2c_Buf_Write中顺序递增的数据写入EERPOM中 
    LED1(ON);
    I2C_EE_BufferWrite( I2c_Buf_Write, EEP_Firstpage, 256);
    LED1(OFF);   

    printf("\r\n写成功\r\n");

    printf("\r\n读出的数据\r\n");
    //将EEPROM读出数据顺序保持到I2c_Buf_Read中
    LED2(ON);
    I2C_EE_BufferRead(I2c_Buf_Read, EEP_Firstpage, 256); 
    LED2(OFF);

    //将I2c_Buf_Read中的数据通过串口打印
    for (i=0; i<256; i++)
    {
        if(I2c_Buf_Read[i] != I2c_Buf_Write[i])
        {
            printf("0x%02X ", I2c_Buf_Read[i]);
            printf("错误:I2C EEPROM写入与读出的数据不一致\n\r");
            return;
        }
        printf("0x%02X ", I2c_Buf_Read[i]);
        if(i%16 == 15)
        {
            printf("\r\n");
        }
    }
    printf("I2C(AT24C02)读写测试成功\r\n");
}

21.5.2具体代码实现-HAL库

21.5.2.1硬件I2C配置

打开工程,首先是使能I2C1。

ppzc4OS.md.png

接下来就是配置I2C,本文是通过I2C读取EEPROM,这里设置为主机模式,使用默认参数即可。

ppzgojK.md.png

配置完成后,即可生成工程。

21.5.2.2具体代码分析

接下来通过对EEPROM的读取来验证I2C,首先对一些HAL库函数进行再次封装。

/**
  * @brief  I2C通信错误处理函数
  * @param  DevAddress:I2C设备地址
  *         Trials:尝试测试次数
  * @retval HAL_StatusTypeDef:操作结果
  */
static void I2C_EEPROM_Error (void)
{
    /* 反初始化I2C通信总线 */
    HAL_I2C_DeInit(&hi2c1);

    /* 重新初始化I2C通信总线*/
    MX_I2C1_Init();
    printf("EEPROM I2C通信超时!!! 重新启动I2C...\n");
}

/**
  * @brief  通过I2C写入一个值到指定寄存器内
  * @param  Addr:I2C设备地址
  *         Reg:目标寄存器
  *         Value:值
  * @retval 无
  */
void I2C_EEPROM_WriteData(uint16_t Addr, uint8_t Reg, uint8_t Value)
{
    HAL_StatusTypeDef status = HAL_OK;

    status = HAL_I2C_Mem_Write(&hi2c1, Addr, (uint16_t)Reg, I2C_MEMADD_SIZE_8BIT, &Value, 1, I2cxTimeout);

    /* 检测I2C通信状态 */
    if(status != HAL_OK)
    {
        /* 调用I2C通信错误处理函数 */
        I2C_EEPROM_Error();
    }
}

/**
  * @brief  通过I2C写入一段数据到指定寄存器内
  * @param  Addr:I2C设备地址
  *         Reg:目标寄存器
  *         RegSize:寄存器尺寸(8位或者16位)
  *         pBuffer:缓冲区指针
  *         Length:缓冲区长度
  * @retval HAL_StatusTypeDef:操作结果
  */
HAL_StatusTypeDef I2C_EEPROM_WriteBuffer(uint16_t Addr, uint8_t Reg, uint16_t RegSize, uint8_t *pBuffer, uint16_t Length)
{
    HAL_StatusTypeDef status = HAL_OK;

    status = HAL_I2C_Mem_Write(&hi2c1, Addr, (uint16_t)Reg, RegSize, pBuffer, Length, I2cxTimeout); 

    /* 检测I2C通信状态 */
    if(status != HAL_OK)
    {
        /* 调用I2C通信错误处理函数 */
        I2C_EEPROM_Error();
    }
    return status;
}
/**
  * @brief  通过I2C读取一个指定寄存器内容
  * @param  Addr:I2C设备地址
  *         Reg:目标寄存器
  * @retval uint8_t:寄存器内容
  */
uint8_t I2C_EEPROM_ReadData(uint16_t Addr, uint8_t Reg)
{
    HAL_StatusTypeDef status = HAL_OK;
    uint8_t value = 0;

    status = HAL_I2C_Mem_Read(&hi2c1, Addr, Reg, I2C_MEMADD_SIZE_8BIT, &value, 1, I2cxTimeout);

    /* 检测I2C通信状态 */
    if(status != HAL_OK)
    {
        /* 调用I2C通信错误处理函数 */
        I2C_EEPROM_Error();

    }
    return value;
}

/**
  * @brief  通过I2C读取一段寄存器内容存放到指定的缓冲区内
  * @param  Addr:I2C设备地址
  *         Reg:目标寄存器
  *         RegSize:寄存器尺寸(8位或者16位)
  *         pBuffer:缓冲区指针
  *         Length:缓冲区长度
  * @retval HAL_StatusTypeDef:操作结果
  */
HAL_StatusTypeDef I2C_EEPROM_ReadBuffer(uint16_t Addr, uint8_t Reg, uint16_t RegSize, uint8_t *pBuffer, uint16_t Length)
{
    HAL_StatusTypeDef status = HAL_OK;

    status = HAL_I2C_Mem_Read(&hi2c1, Addr, (uint16_t)Reg, RegSize, pBuffer, Length, I2cxTimeout);

    /* 检测I2C通信状态 */
    if(status != HAL_OK)
    {
        /* 调用I2C通信错误处理函数 */
        I2C_EEPROM_Error();
    }
    return status;
}

/**
  * @brief  检测I2C设备是否处于准备好可以通信状态
  * @param  DevAddress:I2C设备地址
  *         Trials:尝试测试次数
  * @retval HAL_StatusTypeDef:操作结果
  */
HAL_StatusTypeDef I2C_EEPROM_IsDeviceReady(uint16_t DevAddress, uint32_t Trials)
{ 
    return (HAL_I2C_IsDeviceReady(&hi2c1, DevAddress, Trials, I2cxTimeout));
}

/**
  * @brief  I2C(AT24C02)读写测试
  * @param  无
  * @retval 无
  */
void I2C_Test(void)
{
    uint16_t i;

    printf("写入的数据\r\n");

    for ( i=0; i<256; i++ ) //填充缓冲
    {
        I2c_Buf_Write[i] = i;

        printf("0x%02X ", I2c_Buf_Write[i]);
        if(i%16 == 15)    
            printf("\r\n");    
    }

    //将I2c_Buf_Write中顺序递增的数据写入EERPOM中 
    for(i=0;i<256;i+=8)
    {
        I2C_EEPROM_WriteBuffer(EEPROM_I2C_ADDRESS,i,I2C_MEMADD_SIZE_8BIT,&I2c_Buf_Write[i],8);
        HAL_Delay(5);// 短延
    }

    printf("\r\n写成功\r\n");
    printf("\r\n读出的数据\r\n");
    //将EEPROM读出数据顺序保持到I2c_Buf_Read中
    I2C_EEPROM_ReadBuffer(EEPROM_I2C_ADDRESS,0,I2C_MEMADD_SIZE_8BIT,I2c_Buf_Read,256);

    //将I2c_Buf_Read中的数据通过串口打印
    for (i=0; i<256; i++)
    {
        if(I2c_Buf_Read[i] != I2c_Buf_Write[i])
        {
            printf("0x%02X ", I2c_Buf_Read[i]);
            printf("错误:I2C EEPROM写入与读出的数据不一致\r\n");
            return;
        }
        printf("0x%02X ", I2c_Buf_Read[i]);
        if(i%16 == 15)
            printf("\r\n");
    }
    printf("I2C(AT24C02)读写测试成功\r\n");
}

值得注意的是,AT24C02的2Kbit分为32页,每页8个字节。而EEPROM有一种写入方式就是按页写入,而不是按字节写入。上述封装的函数是按页写入的方式。注意每次写入完毕需要延时5ms,这是AT24C02的要求。

ppORtB9.md.png

在前面的章节已经对AT24C02的时序进行了详细阐述,这里只是使用HAL库实现,注释也很清楚,我就不在赘述了。
接下来就是通过以上函数来实现EEPROM的读写操作。

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  MX_I2C1_Init();
  /* USER CODE BEGIN 2 */

  printf("******** EEPROM(AT24C02)数据读写(硬件I2C模式)测试 \r\n");
  I2C_Test();

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

主函数很简单,就是调用封装的函数即可。

21.5.3实验现象

下载好程序后,打开串口助手,可以看到如下信息。

p9SdsA0.md.png

最后,我们使用逻辑分析来查看数据。

p9SwfG8.md.png

我这里使用的HAL库,使用的100kHz的速率,可以看到数据的写操作和前面分析的时序是一样的,完全吻合。

21.6软件I2C

21.6.1具体代码实现-标准库

首先实现I2C的协议。

/**
  * @brief  I2C_Delay, I2C总线位延迟,最快400KHz
  * @param  None
  * @retval None
  */
static void I2C_Delay(void)
{
    uint8_t i;

    /*
      CPU主频72MHz时,在内部Flash运行, MDK工程不优化
      循环次数为10时,SCL频率 = 205KHz 
      循环次数为7时,SCL频率 = 347KHz, SCL高电平时间1.5us,SCL低电平时间2.87us 
      循环次数为5时,SCL频率 = 421KHz, SCL高电平时间1.25us,SCL低电平时间2.375us 

      IAR工程编译效率高,不能设置为7
    */
    for (i = 0; i < 10; i++);
}

/**
  * @brief  I2C_Start, CPU发起I2C总线启动信号
  * @param  None
  * @retval None
  */
void I2C_Start(void)
{
    /* 当SCL高电平时,SDA出现一个下跳沿表示I2C总线启动信号 */
    I2C_SDA_1();
    I2C_SCL_1();
    I2C_Delay();
    I2C_SDA_0();//START:when CLK is high,DATA change form high to low 
    I2C_Delay();
    I2C_SCL_0();
    I2C_Delay();
}

/**
  * @brief  I2C_Stop, CPU发起I2C总线停止信号
  * @param  None
  * @retval None
  */
void I2C_Stop(void)
{
    /* 当SCL高电平时,SDA出现一个上跳沿表示I2C总线停止信号 */
    I2C_SDA_0();//STOP:when CLK is high DATA change form low to high
    I2C_SCL_1();
    I2C_Delay();
    I2C_SDA_1();
}

/**
  * @brief  I2C_SendByte, CPU向I2C总线设备发送8bit数据
  * @param  _ucByte : 等待发送的字节
  * @retval None
  */
void I2C_SendByte(uint8_t _ucByte)
{
    uint8_t i;

    /* 先发送字节的高位bit7 */
    for (i = 0; i < 8; i++)
    {
        if (_ucByte & 0x80)
        {
            I2C_SDA_1();
        }
        else
        {
            I2C_SDA_0();
        }
        I2C_Delay();
        I2C_SCL_1();
        I2C_Delay();    
        I2C_SCL_0();
        if (i == 7)
        {
            I2C_SDA_1(); // 释放总线
        }
        _ucByte <<= 1;    /* 左移一个bit */
        I2C_Delay();
    }
}

/**
  * @brief  I2C_ReadByte, CPU从I2C总线设备读取8bit数据
  * @param  None
  * @retval 读到的数据
  */
uint8_t I2C_ReadByte(void)
{
    uint8_t i;
    uint8_t value;

    /* 读到第1个bit为数据的bit7 */
    value = 0;
    for (i = 0; i < 8; i++)
    {
        value <<= 1;
        I2C_SCL_1();
        I2C_Delay();
        if (I2C_SDA_READ())
        {
            value++;
        }
        I2C_SCL_0();
        I2C_Delay();
    }
    return value;
}

/**
  * @brief  I2C_WaitAck, CPU产生一个时钟,并读取器件的ACK应答信号
  * @param  None
  * @retval 返回0表示正确应答,1表示无器件响应
  */
uint8_t I2C_WaitAck(void)
{
    uint8_t re;

    I2C_SDA_1();    /* CPU释放SDA总线 */
    I2C_Delay();
    I2C_SCL_1();    /* CPU驱动SCL = 1, 此时器件会返回ACK应答 */
    I2C_Delay();
    if (I2C_SDA_READ())    /* CPU读取SDA口线状态 */
    {
        re = 1;
    }
    else
    {
        re = 0;
    }
    I2C_SCL_0();
    I2C_Delay();
    return re;
}

/**
  * @brief  I2C_Ack, CPU产生一个ACK信号
  * @param  None
  * @retval None
  */
void I2C_Ack(void)
{
    I2C_SDA_0();    /* CPU驱动SDA = 0 */
    I2C_Delay();
    I2C_SCL_1();    /* CPU产生1个时钟 */
    I2C_Delay();
    I2C_SCL_0();
    I2C_Delay();
    I2C_SDA_1();    /* CPU释放SDA总线 */
}

/**
  * @brief  iI2C_NAck, CPU产生1个NACK信号
  * @param  None
  * @retval None
  */
void I2C_NAck(void)
{
    I2C_SDA_1();    /* CPU驱动SDA = 1 */
    I2C_Delay();
    I2C_SCL_1();    /* CPU产生1个时钟 */
    I2C_Delay();
    I2C_SCL_0();
    I2C_Delay();    
}

/**
  * @brief  I2C_Cfg_GPIO, 配置I2C总线的GPIO,采用模拟IO的方式实现
  * @param  None
  * @retval None
  */
static void I2C_Cfg_GPIO(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_I2C_PORT, ENABLE);    /* 打开GPIO时钟 */

    GPIO_InitStructure.GPIO_Pin = I2C_SCL_PIN | I2C_SDA_PIN;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;      /* 开漏输出 */
    GPIO_Init(GPIO_PORT_I2C, &GPIO_InitStructure);

    /* 给一个停止信号, 复位I2C总线上的所有设备到待机模式 */
    I2C_Stop();
}

/**
  * @brief  i2c_CheckDevice, 检测I2C总线设备,CPU向发送设备地址,然后读取设备应答来判断该设备是否存在
  * @param  _Address:设备的I2C总线地址
  * @retval 返回值 0 表示正确, 返回1表示未探测到
  */
uint8_t I2C_CheckDevice(uint8_t _Address)
{
    uint8_t ucAck;

    I2C_Cfg_GPIO();        /* 配置GPIO */

    I2C_Start();        /* 发送启动信号 */

    /* 发送设备地址+读写控制bit(0 = w, 1 = r) bit7 先传 */
    I2C_SendByte(_Address | I2C_WR);
    ucAck = I2C_WaitAck();    /* 检测设备的ACK应答 */

    I2C_Stop();            /* 发送停止信号 */

    return ucAck;
}

注释很清楚,对照I2C的协议看就行。

接着就是实现AT2C02的读写操作。

/**
  * @brief  EEPROM_CheckOk, 判断串行EERPOM是否正常
  * @param  None
  * @retval 1 表示正常, 0 表示不正常
  */
uint8_t EEPROM_CheckOk(void)
{
    if (I2C_CheckDevice(EEPROM_DEV_ADDR) == 0)
    {
        return 1;
    }
    else
    {
        /* 失败后,切记发送I2C总线停止信号 */
        I2C_Stop();
        return 0;
    }
}

/**
  * @brief  EEPROM_ReadBytes, 从串行EEPROM指定地址处开始读取若干数据
  * @param  _usAddress : 起始地址
  *            _usSize : 数据长度,单位为字节
  *         _pReadBuf : 存放读到的数据的缓冲区指针
  * @retval 0 表示失败,1表示成功
  */
uint8_t EEPROM_ReadBytes(uint8_t *_pReadBuf, uint16_t _usAddress, uint16_t _usSize)
{
    uint16_t i;

    /* 采用串行EEPROM随即读取指令序列,连续读取若干字节 */

    /* 第1步:发起I2C总线启动信号 */
    I2C_Start();

    /* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
    I2C_SendByte(EEPROM_DEV_ADDR | I2C_WR);    /* 此处是写指令 */

    /* 第3步:发送ACK */
    if (I2C_WaitAck() != 0)
    {
        goto cmd_fail;    /* EEPROM器件无应答 */
    }

    /* 第4步:发送字节地址,24C02只有256字节,因此1个字节就够了,如果是24C04以上,那么此处需要连发多个地址 */
    I2C_SendByte((uint8_t)_usAddress);

    /* 第5步:发送ACK */
    if (I2C_WaitAck() != 0)
    {
        goto cmd_fail;    /* EEPROM器件无应答 */
    }

    /* 第6步:重新启动I2C总线。前面的代码的目的向EEPROM传送地址,下面开始读取数据 */
    I2C_Start();

    /* 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
    I2C_SendByte(EEPROM_DEV_ADDR | I2C_RD);    /* 此处是读指令 */

    /* 第8步:发送ACK */
    if (I2C_WaitAck() != 0)
    {
        goto cmd_fail;    /* EEPROM器件无应答 */
    }    

    /* 第9步:循环读取数据 */
    for (i = 0; i < _usSize; i++)
    {
        _pReadBuf[i] = I2C_ReadByte();    /* 读1个字节 */

        /* 每读完1个字节后,需要发送Ack, 最后一个字节不需要Ack,发Nack */
        if (i != _usSize - 1)
        {
            I2C_Ack();    /* 中间字节读完后,CPU产生ACK信号(驱动SDA = 0) */
        }
        else
        {
            I2C_NAck();    /* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */
        }
    }
    /* 发送I2C总线停止信号 */
    I2C_Stop();
    return 1;    /* 执行成功 */

cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
    /* 发送I2C总线停止信号 */
    I2C_Stop();
    return 0;
}

/**
  * @brief  EEPROM_WriteBytes, 向串行EEPROM指定地址写入若干数据,采用页写操作提高写入效率
  * @param  _usAddress : 起始地址
  *            _usSize : 数据长度,单位为字节
  *         _pWriteBuf : 存放读到的数据的缓冲区指针
  * @retval 0 表示失败,1表示成功
  */
uint8_t EEPROM_WriteBytes(uint8_t *_pWriteBuf, uint16_t _usAddress, uint16_t _usSize)
{
    uint16_t i,m;
    uint16_t usAddr;

    /* 
        写串行EEPROM不像读操作可以连续读取很多字节,每次写操作只能在同一个page。
        对于24xx02,page size = 8
        简单的处理方法为:按字节写操作模式,没写1个字节,都发送地址
        为了提高连续写的效率: 本函数采用page wirte操作。
    */

    usAddr = _usAddress;
    for (i = 0; i < _usSize; i++)
    {
        /* 当发送第1个字节或是页面首地址时,需要重新发起启动信号和地址 */
        if ((i == 0) || (usAddr & (EEPROM_PAGE_SIZE - 1)) == 0)
        {
            /* 第0步:发停止信号,启动内部写操作 */
            I2C_Stop();

            /* 通过检查器件应答的方式,判断内部写操作是否完成, 一般小于 10ms
                CLK频率为200KHz时,查询次数为30次左右
            */
            for (m = 0; m < 100; m++)
            {
                /* 第1步:发起I2C总线启动信号 */
                I2C_Start();

                /* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
                I2C_SendByte(EEPROM_DEV_ADDR | I2C_WR);    /* 此处是写指令 */

                /* 第3步:发送一个时钟,判断器件是否正确应答 */
                if (I2C_WaitAck() == 0)
                {
                    break;
                }
            }
            if (m  == 1000)
            {
                goto cmd_fail;    /* EEPROM器件写超时 */
            }

            /* 第4步:发送字节地址,24C02只有256字节,因此1个字节就够了,如果是24C04以上,那么此处需要连发多个地址 */
            I2C_SendByte((uint8_t)usAddr);

            /* 第5步:发送ACK */
            if (I2C_WaitAck() != 0)
            {
                goto cmd_fail;    /* EEPROM器件无应答 */
            }
        }

        /* 第6步:开始写入数据 */
        I2C_SendByte(_pWriteBuf[i]);

        /* 第7步:发送ACK */
        if (I2C_WaitAck() != 0)
        {
            goto cmd_fail;    /* EEPROM器件无应答 */
        }

        usAddr++;    /* 地址增1 */
    }

    /* 命令执行成功,发送I2C总线停止信号 */
    I2C_Stop();
    return 1;

cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
    /* 发送I2C总线停止信号 */
    I2C_Stop();
    return 0;
}

/**
  * @brief  EEPROM_Erase
  * @param  None
  * @retval None
  */
void EEPROM_Erase(void)
{
    uint16_t i;
    uint8_t buf[EEPROM_SIZE];

    /* 填充缓冲区 */
    for (i = 0; i < EEPROM_SIZE; i++)
    {
        buf[i] = 0xFF;
    }

    /* 写EEPROM, 起始地址 = 0,数据长度为 256 */
    if (EEPROM_WriteBytes(buf, 0, EEPROM_SIZE) == 0)
    {
        printf("擦除eeprom出错!\r\n");
        return;
    }
    else
    {
        printf("擦除eeprom成功!\r\n");
    }
}

/**
  * @brief  EE_Delay
  * @param  nCount
  * @retval None
  */
static void EEPROM_Delay(__IO uint32_t nCount)     //简单的延时函数
{
    for(; nCount != 0; nCount--);
}

/**
  * @brief  AT24C02 读写测试
  * @param  None
  * @retval None
  */
void EEPROM_Test(void)
{
    uint16_t i;
    uint8_t write_buf[EEPROM_SIZE];
    uint8_t read_buf[EEPROM_SIZE];

    /*-----------------------------------------------------------------------------------*/  
    if (EEPROM_CheckOk() == 0)
    {
        /* 没有检测到EEPROM */
        printf("没有检测到串行EEPROM!\r\n");
        while (1);    /* 停机 */
    }
    /*------------------------------------------------------------------------------------*/  
    /* 填充测试缓冲区 */
    for (i = 0; i < EEPROM_SIZE; i++)
    {
        write_buf[i] = i;
    }
    /*------------------------------------------------------------------------------------*/  
    if (EEPROM_WriteBytes(write_buf, 0, EEPROM_SIZE) == 0)
    {
        printf("写eeprom出错!\r\n");
        return;
    }
    else
    {
        printf("写eeprom成功!\r\n");
    }

    /*写完之后需要适当的延时再去读,不然会出错*/
    EEPROM_Delay(0x0FFFFF);
    /*-----------------------------------------------------------------------------------*/
    if (EEPROM_ReadBytes(read_buf, 0, EEPROM_SIZE) == 0)
    {
        printf("读eeprom出错!\r\n");
        return;
    }
    else
    {
        printf("读eeprom成功,数据如下:\r\n");
    }
    /*-----------------------------------------------------------------------------------*/  
    for (i = 0; i < EEPROM_SIZE; i++)
    {
        if(read_buf[i] != write_buf[i])
        {
            printf("0x%02X ", read_buf[i]);
            printf("错误:EEPROM读出与写入的数据不一致");
            return;
        }
        printf(" %02X", read_buf[i]);

        if ((i & 15) == 15)
        {
            printf("\r\n");    
        }
    }
    printf("eeprom读写测试成功\r\n");
    while(1);
}

代码很简单,和使用硬件I2C的逻辑是一样的。

最后看下主函数吧。

/**
  * @brief  主函数
  * @param  无
  * @retval 无
  */
int main(void)
{
    /*SysTick Init*/
    SysTick_Init();

    /* USART1 config 115200 8-N-1 */
    USART_Config();

    printf("eeprom 软件模拟i2c测试例程 \r\n");

    EEPROM_Test();

    for(;;)
    {

    }
}

主函数中重点关注EEPROM_Test()函数,这就是对AT24C02的读写操作。

21.6.2具体代码实现-HAL库

21.6.2.1软件I2C配置

软件I2C只需要将PB6和PB7配置成普通IO即可,输出模式设置为开漏输出,速度设置为中速。

p9Sw4xg.md.png

配置完成后,即可生成工程。

当然,也可以自行初始化。笔者这部分代码放在了GPIO模拟I2C的代码中了。

21.6.2.2具体代码分析

代码如下:

void EEPROM_Test(void)
{
    uint16_t i;
    uint8_t flag;
    flag = EEPROM_CheckOk();

    if(flag == 1)
    {
        printf("检测到板载EEPROM(AT24C02)芯片\r\n");
        printf("待写入的数据:\r\n");
        for ( i=0; i<256; i++ ) //填充缓冲
        {
             I2c_Buf_Read[i]=0;      // 清空接收缓冲区
             I2c_Buf_Write[i] = i;   // 为发送缓冲区填充数据
             printf("0x%02X ", I2c_Buf_Write[i]);
             if(i%16 == 15)    
                 printf("\r\n");
        }
        //将I2c_Buf_Write中顺序递增的数据写入EERPOM中 
        EEPROM_WriteBytes(I2c_Buf_Write, 0, 256);  
        HAL_Delay(100);

        printf("读出的数据:\r\n");
        //将EEPROM读出数据顺序保持到I2c_Buf_Read中  
        EEPROM_ReadBytes(I2c_Buf_Read, 0, 256); 
        for (i=0;i<256;i++)
        {
            if(I2c_Buf_Read[i] != I2c_Buf_Write[i])
            {
                printf("0x%02X ", I2c_Buf_Read[i]);
                printf("错误:I2C EEPROM写入与读出的数据不一致\r\n");
                break;
            }
            printf("0x%02X ", I2c_Buf_Read[i]);
            if(i%16 == 15)    
                printf("\r\n");
        }
        if(i==256)
        {
            printf("EEPROM(AT24C02)读写测试成功\n\r");
        }
    }
}

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  printf("EEPROM(AT24C02)数据读写(软件模拟I2C模式)测试 \r\n");
  EEPROM_Test();
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

笔者这里只贴出最终调用的部分,关于软件I2C的部分请参看源码,和硬件I2C不同的是我们通过软件的方式控制时序实现I2C,整个大的逻辑都是一样的,当然网上也有很多相关的例程,有兴趣的自行搜索吧。

21.6.3实验现象

下载程序,连接串口打印信息如下。

p9Sworj.md.png

Related posts

7 Thoughts to “【ARM Cortex-M开发实战指南(基础篇)】第21章 I2C”

  1. Yes! Finally somwone writes about 32589.

  2. Hello Dear, are you inn fact viiting this ste
    regularly, iif sso after tat youu will without dobt gett good experience.

Leave a Comment